SPB Git forge

spb/rareindex

Public
54commits 1branches 0releases
7.1 MBsize
maindefault branch
10 days agolast push
TypeScript 61.9% HTML 37.2% SQL 0.7%
3.1 KB · 59 lines tsx
Raw Blame History
1import type { Metadata } from 'next';2import { notFound } from 'next/navigation';3import { PageHeader } from '@/components/ui/page-header';4import { Stat } from '@/components/ui/primitives';5import { AssetBrowser } from '@/components/market/asset-browser';6import { Suspense } from 'react';7import { Skeleton } from '@/components/ui/primitives';8import { getSet, getSetStats } from '@/lib/queries/markets';9import { catName, categoryCrumbs } from '@/lib/taxonomy';10import { fmtMoney, fmtNum, fmtRelative } from '@/lib/format';11import type { SP } from '@/lib/search-params';1213export const revalidate = 300;1415export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }): Promise<Metadata> {16  const { slug } = await params;17  const s = await getSet(slug);18  if (!s) return { title: 'Set not found' };19  return { title: `${s.name} · ${catName(s.categorySlug)} set`, description: `${s.name}${s.releaseYear ? ` (${s.releaseYear})` : ''}: every tracked asset with valuation, sales and listings.` };20}2122export default async function SetPage({ params, searchParams }: { params: Promise<{ slug: string }>; searchParams: Promise<SP> }) {23  const { slug } = await params;24  const sp = await searchParams;25  const set = await getSet(slug);26  if (!set) notFound();27  const stats = await getSetStats(slug);28  return (29    <div>30      <PageHeader31        crumbs={[{ label: 'Markets', href: '/markets' }, ...categoryCrumbs(set.categorySlug), { label: set.name }]}32        title={set.name}33        description={`${catName(set.categorySlug)} set${set.code ? ` · code ${set.code}` : ''}${set.releaseDate ? ` · released ${set.releaseDate}` : set.releaseYear ? ` · ${set.releaseYear}` : ''}${set.totalItems ? ` · ${set.totalItems} items in the official set` : ''}${set.language ? ` · ${set.language.toUpperCase()}` : ''}.`}34      />35      <section className="grid grid-cols-2 gap-px overflow-hidden rounded-md border border-border bg-border sm:grid-cols-4 lg:grid-cols-7">36        {[37          ['Tracked assets', fmtNum(stats.assets)],38          ['With valuation', fmtNum(stats.priced)],39          ['Sales', fmtNum(stats.sales)],40          ['Active listings', fmtNum(stats.listings)],41          ['Median RIV', stats.medianRivUsd != null ? fmtMoney(stats.medianRivUsd) : '—'],42          ['Top RIV', stats.maxRivUsd != null ? fmtMoney(stats.maxRivUsd) : '—'],43          ['Last sale', stats.lastSaleAt ? fmtRelative(stats.lastSaleAt) : '—'],44        ].map(([k, v]) => (45          <div key={k} className="bg-elevated px-3 py-2.5">46            <Stat label={k} value={v} />47          </div>48        ))}49      </section>50      {stats.sumRivUsd != null && stats.priced === stats.assets && stats.assets > 0 ? <p className="mt-1 text-[11px] text-subtle">Sum of RIV across all {stats.assets} priced assets: {fmtMoney(stats.sumRivUsd)} (a complete-set value indication, not a market cap).</p> : null}51      <div className="mt-4">52        <Suspense fallback={<Skeleton className="h-96" />}>53          <AssetBrowser scope={{ set: slug }} sp={{ ...sp, sort: sp.sort ?? 'number' }} basePath={`/set/${slug}`} title={`Assets in ${set.name}`} pageSize={60} />54        </Suspense>55      </div>56    </div>57  );58}59